package com.appspot.gaebirds;

import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.*;

import twitter4j.Paging;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.http.AccessToken;
import twitter4j.http.RequestToken;

@SuppressWarnings("serial")
public class TwitterWebClientServlet extends HttpServlet {
	private static final Logger logger = Logger
			.getLogger(TwitterWebClientServlet.class.getName());

	private static final String INIT_PARAM_CONSUMER_KEY = "consumerKey";
	private static final String INIT_PARAM_CONSUMER_SECRET = "consumerSecret";

	private static String consumerKey;
	private static String consumerSecret;

	private static final String PARAM_ACTION = "action";
	private static final String ACTION_SIGN_IN = "sign_in";
	private static final String ACTION_UPDATE = "update";
	private static final String ACTION_DELETE = "delete";
	private static final String ACTION_MORE = "more";
	private static final String ACTION_LATEST = "latest";
	private static final String PARAM_OAUTH_TOKEN = "oauth_token";
	private static final String PARAM_STATUS = "status";

	private static final String ATTR_REQUEST_TOKEN = "request_token";
	private static final String ATTR_ACCESS_TOKEN = "access_token";
	private static final String ATTR_LAST_UPDATED_STATUS = "last_udpated_status";
	private static final String ATTR_LAST_UPDATED_STATUS_ID = "last_udpated_status_id";
	private static final String ATTR_FRIENDS_TIMELINES = "friends_timelines";
	private static final String ATTR_FRIENDS_TIMELINES_PAGE = "friends_timelines_page";

	private static final String PAGE_UPDATE_STATUS = "update_twitter_status.jsp";

	private static final String COOKIE_TWITTER_ID = "twitter_id";

	private static final String SEPARATOR = "&";

	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);

		consumerKey = config.getInitParameter(INIT_PARAM_CONSUMER_KEY);
		if (consumerKey == null || consumerKey.trim().equals("")) {
			logger.severe("Consumer key is not defined in web.xml");
		} else {
			logger.fine("Consumer key retrieved from web.xml");
		}
		consumerSecret = config.getInitParameter(INIT_PARAM_CONSUMER_SECRET);
		if (consumerSecret == null || consumerSecret.trim().equals("")) {
			logger.severe("Consumer secret is not defined in web.xml");
		} else {
			logger.fine("Consumer secret retrieved from web.xml");
		}
	}

	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		doPost(req, resp);
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		HttpSession session = req.getSession(false);
		if (session == null) {
			session = req.getSession(true);
			logger.fine("Session is null, new session created");
		}

		// Call back from Twitter
		String oauthToken = req.getParameter(PARAM_OAUTH_TOKEN);
		if (oauthToken != null) {
			logger.fine("Call back from Twitter, " + PARAM_OAUTH_TOKEN
					+ " received");
			String rt = (String) session.getAttribute(ATTR_REQUEST_TOKEN);
			logger.fine("ATTR_REQUEST_TOKEN retrieved from session");
			if (rt == null) {
				logger.severe("ATTR_REQUEST_TOKEN is null");
				throw new ServletException("ATTR_REQUEST_TOKEN is null");
			}

			String[] rttts = rt.split(SEPARATOR);
			Twitter twitter = new Twitter();
			twitter.setOAuthConsumer(consumerKey, consumerSecret);
			try {
				AccessToken accessToken = twitter.getOAuthAccessToken(rttts[0],
						rttts[1]);
				twitter.setOAuthAccessToken(accessToken);

				session.removeAttribute(ATTR_REQUEST_TOKEN);
				logger.fine("ATTR_REQUEST_TOKEN removed from session");
				session.setAttribute(ATTR_ACCESS_TOKEN, accessToken.getToken()
						+ SEPARATOR + accessToken.getTokenSecret());
				logger.fine("ATTR_ACCESS_TOKEN saved in session");

				int id = twitter.verifyCredentials().getId();
				logger.fine("Access token retrieved for user " + id
						+ " from Twitter");

				storeAccessToken("" + id, accessToken);

				// Set cookie, valid for 2 years
				Cookie cookie = new Cookie(COOKIE_TWITTER_ID, "" + id);
				cookie.setMaxAge(63072000); // Valid for 2 years
				resp.addCookie(cookie);
				logger.fine("Cookie set for user " + id);

				// Get last status and friends' timelines
				getMyLastStatusAndStoreInSession(session);
				getFriendsTimelinesAndStoreInSession(session);

				// Go to the update status page
				logger.fine("Going to the update satus page...");
				req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
			} catch (TwitterException e) {
				logger.severe("Failed to retrieve access token - "
						+ e.getMessage());
				throw new ServletException(e);
			}
		}

		String action = req.getParameter(PARAM_ACTION);
		if (ACTION_SIGN_IN.equals(action)) {
			logger.fine("Action: sign in");
			Twitter twitter = new Twitter();
			twitter.setOAuthConsumer(consumerKey, consumerSecret);

			// Try to load Twitter ID from cookies
			String id = null;
			Cookie[] cookies = req.getCookies();
			if (cookies != null) {
				Cookie cookie;
				for (int i = 0; i < cookies.length; i++) {
					cookie = cookies[i];
					if (COOKIE_TWITTER_ID.equals(cookie.getName())) {
						id = cookie.getValue();
						logger.fine("Twitter ID loaded from cookie - " + id);
					}
				}
			}

			// Try to load access token if user's Twitter ID is retrieved
			AccessToken accessToken = null;
			if (id != null) {
				accessToken = loadAccessToken(id);
				if (accessToken != null) {
					logger.fine("Access token loaded from datastore");
					session.setAttribute(ATTR_ACCESS_TOKEN, accessToken
							.getToken()
							+ SEPARATOR + accessToken.getTokenSecret());
					logger.fine("ATTR_ACCESS_TOKEN saved in session");

					twitter.setOAuthAccessToken(accessToken);
					// Get last status and friends' timelines
					try {
						getMyLastStatusAndStoreInSession(session);
						getFriendsTimelinesAndStoreInSession(session, true);
					} catch (TwitterException e) {
						throw new ServletException(e);
					}

					// Go to the update status page
					logger.fine("Going to the update satus page...");
					req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req,
							resp);
				}
			}

			// Can not load access token, go to Twitter for authentication
			if (accessToken == null) {
				try {
					RequestToken requestToken = twitter.getOAuthRequestToken();
					String authorisationUrl = requestToken
							.getAuthorizationURL();
					session.setAttribute(ATTR_REQUEST_TOKEN, requestToken
							.getToken()
							+ SEPARATOR + requestToken.getTokenSecret());
					logger.fine("ATTR_REQUEST_TOKEN saved in session");
					logger.fine("Redirecting user to " + authorisationUrl);
					resp.sendRedirect(authorisationUrl);
				} catch (TwitterException e) {
					logger.fine("Sign in with Twitter failed - "
							+ e.getMessage());
					throw new ServletException(e);
				}
			}

		} else if (ACTION_UPDATE.equals(action)) {
			logger.fine("Action: update");
			String status = req.getParameter(PARAM_STATUS);
			if (status != null && status.trim() != "") {
				Twitter twitter = generateTwitterObjectFromSession(session);

				try {
					int id = twitter.verifyCredentials().getId();
					Status twitterStatus = twitter.updateStatus(status);
					session.setAttribute(ATTR_LAST_UPDATED_STATUS,
							twitterStatus.getText() + " "
									+ twitterStatus.getCreatedAt());
					session.setAttribute(ATTR_LAST_UPDATED_STATUS_ID, ""
							+ twitterStatus.getId());

					// Update friends' timelines
					getFriendsTimelinesAndStoreInSession(session, true);

					logger.fine("Status updated for Twitter user " + id
							+ " to [" + twitterStatus.getText()
							+ "], status stored in session as well");
				} catch (TwitterException e) {
					logger.severe("Failed to update Twitter status - "
							+ e.getMessage());
					throw new ServletException(e);
				}
			}

			// Stay in the update status page
			logger.fine("Staying in the status update page...");
			req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
		} else if (ACTION_DELETE.equals(action)) {
			logger.fine("Action: delete");
			String strId = (String) session
					.getAttribute(ATTR_LAST_UPDATED_STATUS_ID);
			if (strId != null && strId != "") {
				Twitter twitter = generateTwitterObjectFromSession(session);
				try {
					int id = twitter.verifyCredentials().getId();
					twitter.destroyStatus(Long.parseLong(strId));
					session.removeAttribute(ATTR_LAST_UPDATED_STATUS);
					session.removeAttribute(ATTR_LAST_UPDATED_STATUS_ID);
					logger.fine("Last update deleted for Twitter user " + id
							+ " deleted, session record removed");

					// Get last status and friends' timelines
					getMyLastStatusAndStoreInSession(session);
					getFriendsTimelinesAndStoreInSession(session, true);
				} catch (TwitterException e) {
					logger.severe("Failed to delete Twitter status - "
							+ e.getMessage());
					throw new ServletException(e);
				}
			}

			// Stay in the update status page
			logger.fine("Staying in the status update page...");
			req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
		} else if (ACTION_MORE.equals(action)) {
			// Update friends' timelines
			getFriendsTimelinesAndStoreInSession(session);

			// Stay in the update status page
			logger.fine("Staying in the status update page...");
			req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
		} else if (ACTION_LATEST.equals(action)) {
			// Update friends' latest timelines
			getFriendsTimelinesAndStoreInSession(session, true);

			// Stay in the update status page
			logger.info("Staying in the status update page...");
			req.getRequestDispatcher(PAGE_UPDATE_STATUS).forward(req, resp);
		}
	}

	private Twitter generateTwitterObjectFromSession(HttpSession session)
			throws ServletException {
		String at = (String) session.getAttribute(ATTR_ACCESS_TOKEN);
		logger.fine("ATTR_ACCESS_TOKEN retrieved from session");
		if (at == null) {
			logger.severe("ATTR_ACCESS_TOKEN is null");
			throw new ServletException("ATTR_ACCESS_TOKEN is null");
		}
		String[] attts = at.split(SEPARATOR);
		AccessToken accessToken = new AccessToken(attts[0], attts[1]);

		Twitter twitter = new Twitter();
		twitter.setOAuthConsumer(consumerKey, consumerSecret);
		twitter.setOAuthAccessToken(accessToken);

		return twitter;
	}

	private void getMyLastStatusAndStoreInSession(HttpSession session)
			throws TwitterException, ServletException {
		Twitter twitter = generateTwitterObjectFromSession(session);

		// Get last updated status and save it in session
		List<Status> updatesList = twitter.getUserTimeline();
		if (!updatesList.isEmpty()) {
			Status status = updatesList.get(0);
			session.setAttribute(ATTR_LAST_UPDATED_STATUS, status.getText()
					+ " [" + status.getCreatedAt() + "]");
			session.setAttribute(ATTR_LAST_UPDATED_STATUS_ID, ""
					+ status.getId());
			logger.fine("Last update stored in session for Twitter user "
					+ twitter.verifyCredentials().getId());
		} else {
			logger.fine("Twitter user " + twitter.verifyCredentials().getId()
					+ " has no update available");
		}
	}

	private void getFriendsTimelinesAndStoreInSession(HttpSession session,
			boolean isLatestTimelinesRequired) throws ServletException {
		if (isLatestTimelinesRequired) { // Start from page 1
			session.removeAttribute(ATTR_FRIENDS_TIMELINES_PAGE);
			getFriendsTimelinesAndStoreInSession(session);
		}
	}

	private void getFriendsTimelinesAndStoreInSession(HttpSession session)
			throws ServletException {
		Integer p = (Integer) session.getAttribute(ATTR_FRIENDS_TIMELINES_PAGE);
		int page = 1;
		if (p != null) {
			page = p + 1;
		}

		Twitter twitter = generateTwitterObjectFromSession(session);

		try {
			List<Status> l = twitter.getFriendsTimeline(new Paging(page, 20));
			TwitterStatus[] tss = new TwitterStatus[l.size()];
			for (int i = 0; i < tss.length; i++) {
				Status s = l.get(i);
				TwitterStatus ts = new TwitterStatus();
				ts.setCreatedDate(s.getCreatedAt());
				ts.setCreatedUser(s.getUser().getScreenName());
				ts.setId(s.getId());
				ts.setMessage(searchAndReplaceLink(searchAndReplaceLink(s
						.getText(), "http"), "https"));
				tss[i] = ts;
			}
			logger.info(tss.length
					+ " friends' timeline retrieved, page number is " + page);

			session.setAttribute(ATTR_FRIENDS_TIMELINES, tss);
			session.setAttribute(ATTR_FRIENDS_TIMELINES_PAGE, page);
		} catch (TwitterException e) {
			try {
				logger.severe("Failed to get friends' timeline for "
						+ twitter.getId() + " - " + e.getMessage());
			} catch (IllegalStateException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			} catch (TwitterException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}
	}

	private static String searchAndReplaceLink(String message, String protocol) {
		// Message returned by Twitter does not have proper HTTP(S) links
		// So construct them here
		if (message == null || protocol == null) {
			return message;
		}

		protocol = protocol.trim();
		if (protocol.indexOf("://") == -1) {
			protocol = " " + protocol + "://";
		}

		int idx1, idx2;
		StringBuilder sb = new StringBuilder();
		while ((idx1 = message.indexOf(protocol)) != -1) {
			sb.append(message.substring(0, idx1 + 1));
			message = message.substring(idx1 + 1);
			if ((idx2 = message.indexOf(' ')) != -1) {
				String str = message.substring(1, idx2);
				sb.append("<a href=\"").append(str).append(
						"\" target=\"_blank\">").append(str).append("</a>");
				message = message.substring(idx2);
			} else {
				sb.append("<a href=\"").append(message).append(
						"\" target=\"_blank\">").append(message).append("</a>");
				message = "";
			}
		}
		sb.append(message);

		return sb.toString();
	}

	private void storeAccessToken(String id, AccessToken accessToken) {
		if (accessToken == null) {
			throw new IllegalArgumentException(
					"Access token to be saved is null");
		}

		// Notice id starts with a digit number, not allowed
		IdAndAccessToken t = new IdAndAccessToken('$' + id, accessToken
				.getToken(), accessToken.getTokenSecret());
		EntityManager em = JpaUtil.getEmf().createEntityManager();
		try {
			em.getTransaction().begin();
			em.persist(t);
			em.getTransaction().commit();
			logger.fine("Access token saved to datastore for Twitter user "
					+ id);
		} finally {
			em.close();
		}
	}

	private AccessToken loadAccessToken(String id) {
		if (id == null || id.trim().equals("")) {
			throw new IllegalArgumentException("ID to be queried is null");
		}
		EntityManager em = JpaUtil.getEmf().createEntityManager();
		try {
			em.getTransaction().begin();
			// Notice id starts with a digit number, not allowed
			IdAndAccessToken t = em.find(IdAndAccessToken.class, '$' + id);
			em.getTransaction().commit();
			logger.fine("Access token loaded from datastore for Twitter user "
					+ id);
			return new AccessToken(t.getToken(), t.getTokenSecret());
		} finally {
			em.close();
		}
	}
}
